﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.IO.Compression;
using System.Threading;
using NBT;

namespace MCSharp {
    public class Chunk {
        World world;
        public int x, z;
        public List<Player> players = new List<Player>();
        public ChunkBlock[] blocks = new ChunkBlock[16 * 128 * 16];
        public byte[] heightmap = new byte[16 * 16];
        BinaryTag chunkTag = null;
        public bool hasChanged = false;
        public object blockslock = new Object();
        public bool terrainPopulated = false;
        public ReaderWriterLockSlim locker = new ReaderWriterLockSlim();
        public int UseCount = 0;

        #region Constructors

        public Chunk(int x, int z, bool load, World world) {
            this.x = x;
            this.z = z;
            this.world = world;
            if (load) {
                if (!this.Load()) {
                    if (world.flatgrass)
                        LoadFlatgrass();
                    else
                        GenerateWithNoise();
                    Save();
                }
            }
        }

        #endregion

        #region Loading

        public bool Load() {
            if (File.Exists(ChunkFilename(x, z))) {
                lock (blockslock) {
                    try {
                        using (FileStream fs = File.OpenRead(ChunkFilename(x, z))) {
                            using (GZipStream gs = new GZipStream(fs, CompressionMode.Decompress)) {
                                chunkTag = NbtParser.ParseTagStream(gs);
                            }
                        }
                        if (chunkTag == null)
                            return false;
                        int i = 0;
                        foreach (byte type in (byte[])chunkTag["Level"]["Blocks"].Payload) {
                            blocks[i].type = type;
                            blocks[i].metadata = 0x00;
                            blocks[i].lighting = 0x00;
                            i++;
                        }
                        i = 0;
                        foreach (byte height in (byte[])chunkTag["Level"]["HeightMap"].Payload) {
                            heightmap[i] = height;
                            i++;
                        }
                        if (chunkTag["Level"]["Lighting"] != null) {
                            i = 0;
                            foreach (byte light in (byte[])chunkTag["Level"]["Lighting"].Payload) {
                                blocks[i * 2].lighting = (byte)(blocks[i * 2].lighting | ((light & 0xF0) >> 4));
                                blocks[(i * 2) + 1].lighting = (byte)(blocks[(i * 2) + 1].lighting | (light & 0x0F));
                                i++;
                            }
                        }
                        if (chunkTag["Level"]["BlockLight"] != null) {
                            i = 0;
                            foreach (byte light in (byte[])chunkTag["Level"]["BlockLight"].Payload) {
                                blocks[(i * 2) + 1].lighting = (byte)(blocks[(i * 2) + 1].lighting | ((light & 0xF0) >> 4));
                                blocks[(i * 2) + 0].lighting = (byte)(blocks[(i * 2) + 0].lighting | (light & 0x0F));
                                i++;
                            }
                        }
                        if (chunkTag["Level"]["SkyLight"] != null) {
                            i = 0;
                            foreach (byte light in (byte[])chunkTag["Level"]["SkyLight"].Payload) {
                                blocks[(i * 2) + 1].lighting = (byte)(blocks[(i * 2) + 1].lighting | ((light & 0xF0) >> 4));
                                blocks[(i * 2) + 0].lighting = (byte)(blocks[(i * 2) + 0].lighting | (light & 0x0F));
                                i++;
                            }
                        }
                        i = 0;
                        foreach (byte metadata in (byte[])chunkTag["Level"]["Data"].Payload) {
                            blocks[(i * 2) + 1].metadata = (byte)((metadata & 0xF0) >> 4);
                            blocks[(i * 2) + 0].metadata = (byte)(metadata & 0x0F);
                            i++;
                        }
                        terrainPopulated = (chunkTag["Level"]["TerrainPopulated"].GetByte() > 0);
                    } catch (Exception e) {
                        Logger.Log("Failed to load or parse chunk file. Generating flatgrass instead. ({0}, {1}): {2}", LogType.Error, x, z, e);
                        return false;
                    }
                }
                hasChanged = false;
                return true;
            }
            return false;
        }

        #endregion

        #region Saving

        public bool Save() {
            //return true;
            Directory.CreateDirectory("world/chunks");
            if (!terrainPopulated)
                CalculateLighting();
            if (File.Exists(ChunkFilename(x, z)) && chunkTag != null) {
                try {
                    locker.EnterWriteLock();
                    using (FileStream fs = File.OpenWrite(ChunkFilename(x, z))) {
                        using (GZipStream gs = new GZipStream(fs, CompressionMode.Compress)) {
                            int i = 0;
                            foreach (ChunkBlock block in blocks) {
                                ((byte[])chunkTag["Level"]["Blocks"].Payload)[i] = block.type;
                                i++;
                            }
                            i = 0;
                            foreach (byte height in heightmap) {
                                ((byte[])chunkTag["Level"]["HeightMap"].Payload)[i] = heightmap[i];
                                i++;
                            }
                            for (i = 0; i < (16 * 16 * 128 / 2); i++) {
                                ((byte[])chunkTag["Level"]["Data"].Payload)[i] = (byte)(((blocks[i * 2].metadata & 0x0F) << 4) | (blocks[(i * 2) + 1].metadata & 0x0F));
                                ((byte[])chunkTag["Level"]["Lighting"].Payload)[i] = (byte)(((blocks[i * 2].lighting & 0x0F) << 4) | (blocks[(i * 2) + 1].lighting & 0x0F));
                            }
                            chunkTag["Level"]["TerrainPopulated"].Payload = (byte)(terrainPopulated ? 0x01 : 0x00);
                            NbtWriter.WriteTagStream(chunkTag, gs);
                        }
                    }
                    locker.ExitWriteLock();
                } catch (Exception e) {
                    Logger.Log("Failed to save chunk file! ({0}, {1}): {2}", LogType.Error, x, z, e);
                    return false;
                } finally {
                    if (locker.IsWriteLockHeld)
                        locker.ExitWriteLock();
                }
                hasChanged = false;
            } else {
                try {
                    byte[] blockArray = new byte[16 * 16 * 128];
                    byte[] metaArray = new byte[16 * 16 * 128 / 2];
                    byte[] lightArray = new byte[16 * 16 * 128 / 2];
                    int i = 0;
                    foreach (ChunkBlock block in blocks) {
                        blockArray[i] = block.type;
                        i++;
                    }
                    for (i = 0; i < (16 * 16 * 128 / 2); i++) {
                        metaArray[i] = (byte)(((blocks[i * 2].metadata & 0x0F) << 4) | (blocks[(i * 2) + 1].metadata & 0x0F));
                        lightArray[i] = (byte)(((blocks[i * 2].lighting & 0x0F) << 4) | (blocks[(i * 2) + 1].lighting & 0x0F));
                    }

                    BinaryTag[] LevelTag = new BinaryTag[7];
                    LevelTag[0] = new BinaryTag(TagType.Int, x);
                    LevelTag[0].Name = "xPos";
                    LevelTag[1] = new BinaryTag(TagType.Int, z);
                    LevelTag[1].Name = "zPos";
                    LevelTag[2] = new BinaryTag(TagType.Byte, (byte)(terrainPopulated ? 0x01 : 0x00));
                    LevelTag[2].Name = "TerrainPopulated";
                    LevelTag[3] = new BinaryTag(TagType.ByteArray, blockArray);
                    LevelTag[3].Name = "Blocks";
                    LevelTag[4] = new BinaryTag(TagType.ByteArray, metaArray);
                    LevelTag[4].Name = "Data";
                    LevelTag[5] = new BinaryTag(TagType.ByteArray, lightArray);
                    LevelTag[5].Name = "Lighting";
                    LevelTag[6] = new BinaryTag(TagType.ByteArray, heightmap);
                    LevelTag[6].Name = "HeightMap";

                    BinaryTag[] RootTag = new BinaryTag[1];
                    RootTag[0] = new BinaryTag(TagType.Compound, LevelTag);
                    RootTag[0].Name = "Level";

                    chunkTag = new BinaryTag(TagType.Compound, RootTag);

                    locker.EnterWriteLock();
                    using (FileStream fs = File.OpenWrite(ChunkFilename(x, z))) {
                        using (GZipStream gs = new GZipStream(fs, CompressionMode.Compress)) {
                            NbtWriter.WriteTagStream(chunkTag, gs);
                        }
                    }
                    locker.ExitWriteLock();
                    hasChanged = false;
                } catch (Exception e) {
                    Logger.Log("Failed to save chunk file! ({0}, {1}): {2}", LogType.Error, x, z, e);
                    return false;
                } finally {
                    if(locker.IsWriteLockHeld)
                        locker.ExitWriteLock();
                }
            }
            return true;
        }

        #endregion

        #region Generation

        void LoadFlatgrass() {
            lock (blockslock) {
                for (byte bX = 0; bX < 16; bX++) {
                    for (byte bY = 0; bY < 128; bY++) {
                        for (byte bZ = 0; bZ < 16; bZ++) {
                            if (bY == 0) {
                                blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Admincrete, 0x00, 0x00);
                            } else if (bY < 64) {
                                blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Dirt, 0x00, 0x00);
                            } else if (bY == 64) {
                                if ((x == -1 && z == 0) && false) {
                                    if(bX == 0 && bZ == 15)
                                        blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Leaves, 0x00, 0xFF);
                                    else
                                        blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Sand, 0x00, 0xFF);
                                } else {
                                    blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Grass, 0x00, 0xFF);
                                }
                            } else {
                                blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Air, 0x00, 0xFF);
                            }
                        }
                    }
                }
                CalculateHeightmap();
            }
        }

        void GenerateWithNoise() {
            // Ore arrays
            byte[] oreX, oreY, oreZ, oreType;

            // Get main heightmap
            float[,] heightMap = new float[16, 16];
            Noise noise = new Noise((int)world.randomSeed, NoiseInterpolationMode.Bicubic);
            noise.PerlinNoiseMap(heightMap, -2, 2, 0.4f, x, z);

            // Get secondary heightmap
            float[,] steepnessMap = new float[16, 16];
            Noise steepnessNoise = new Noise((int)world.randomSeed, NoiseInterpolationMode.Cosine);
            steepnessNoise.PerlinNoiseMap(steepnessMap, -9, 0, 0.6f, x, z);
            for (int i = 0; i < 16; i++) {
                for (int j = 0; j < 16; j++) {
                    steepnessMap[i, j] = steepnessMap[i, j] / 10;
                }
            }

            // Get cave heightmaps for upper caves (top & bottom)
            float[,] caveTop = new float[16, 16];
            Noise caveTopNoise = new Noise((int)world.randomSeed + new Random((int)world.randomSeed).Next(), NoiseInterpolationMode.Spline);
            caveTopNoise.PerlinNoiseMap(caveTop, 0, 4, 0.4f, x, z);
            Noise.Invert(caveTop);
            Noise.Marble(caveTop);
            float[,] caveBottom = new float[16, 16];
            Noise caveBottomNoise = new Noise((int)world.randomSeed + new Random((int)new Random((int)world.randomSeed).Next()).Next(), NoiseInterpolationMode.Spline);
            caveBottomNoise.PerlinNoiseMap(caveBottom, 0, 4, 0.2f, x, z);
            Noise.Invert(caveBottom);
            Noise.Marble(caveBottom);

            // Get cave heightmaps for lower caves (top & bottom)
            float[,] caveTop2 = new float[16, 16];
            Noise caveTopNoise2 = new Noise((int)world.randomSeed + new Random((int)world.randomSeed + 1).Next(), NoiseInterpolationMode.Spline);
            caveTopNoise2.PerlinNoiseMap(caveTop2, 0, 4, 0.4f, x, z);
            Noise.Invert(caveTop2);
            Noise.Marble(caveTop2);
            float[,] caveBottom2 = new float[16, 16];
            Noise caveBottomNoise2 = new Noise((int)world.randomSeed + new Random((int)new Random((int)world.randomSeed + 2).Next() + 1).Next(), NoiseInterpolationMode.Spline);
            caveBottomNoise2.PerlinNoiseMap(caveBottom2, 0, 4, 0.2f, x, z);
            Noise.Invert(caveBottom2);
            Noise.Marble(caveBottom2);

            // Determine whether there should be ore (random, not seed-related) and where it should be and what type
            int oreChance = world.rand.Next(0, 10);
            int numOre = 0;
            if (world.oreDensity >= 18) {
                if (oreChance <= 2)
                    numOre = world.oreDensity;
                else if (oreChance <= 4)
                    numOre = world.oreDensity - 4;
                else if (oreChance <= 8)
                    numOre = world.oreDensity - 8;
                else if (oreChance <= 10)
                    numOre = world.oreDensity - 12;
            } else
                numOre = world.oreDensity;
            oreX = new byte[numOre];
            oreY = new byte[numOre];
            oreZ = new byte[numOre];
            oreType = new byte[numOre];
            for (int i = 0; i < numOre; i++) {
                oreX[i] = (byte)world.rand.Next(1, 15);
                oreY[i] = (byte)world.rand.Next(1, 64);
                oreZ[i] = (byte)world.rand.Next(1, 15);
                int oreTypeRand = world.rand.Next(0, 20);
                if (oreY[i] < 14) {
                    if (oreTypeRand < 10)
                        oreType[i] = (byte)Block.RedstoneOre;
                    else
                        oreType[i] = (byte)Block.DiamondOre;
                } else {
                    if (oreTypeRand <= 5 && oreY[i] < 30)
                        oreType[i] = (byte)Block.GoldOre;
                    else if (oreTypeRand <= 9)
                        oreType[i] = (byte)Block.IronOre;
                    else if (oreTypeRand <= 20)
                        oreType[i] = (byte)Block.Coal;
                }
            }

            // Populate blocks in chunk
            int currentHeight, caveTopHeight, caveBottomHeight, caveTopHeight2, caveBottomHeight2;
            lock (blockslock) {
                for (byte bX = 0; bX < 16; bX++) {
                    for (byte bY = 0; bY < 128; bY++) {
                        for (byte bZ = 0; bZ < 16; bZ++) {
                            currentHeight = (int)((heightMap[bX, bZ] * 2.49674) + 62.15371);
                            currentHeight = (int)((heightMap[bX, bZ] * 2.49674) + 82.15371);

                            caveTopHeight = (int)((caveTop[bX, bZ] * 4.29674) + 36.15371);
                            caveBottomHeight = (int)((caveBottom[bX, bZ] * 2.29674) + 40.15371);
                            caveTopHeight += (int)((float)currentHeight / 128f * 90f) - 40;
                            caveBottomHeight += (int)((float)currentHeight / 128f * 90f) - 45;

                            caveTopHeight2 = (int)((caveTop2[bX, bZ] * 4.29674) + 36.15371);
                            caveBottomHeight2 = (int)((caveBottom2[bX, bZ] * 2.29674) + 40.15371);
                            caveTopHeight2 += (int)((float)currentHeight / 128f * 100f) - 70;
                            caveBottomHeight2 += (int)((float)currentHeight / 128f * 100f) - 75;

                            if (bY == 0) {
                                blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Admincrete, 0x00, 0x00);
                            } else {
                                if (currentHeight > bY) {
                                    if (bY < (int)(currentHeight * 0.94)) {
                                        if(bY >= caveBottomHeight && bY <= caveTopHeight)
                                            blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Air, 0x00, 0x00);
                                        else if (bY >= caveBottomHeight2 && bY <= caveTopHeight2)
                                            blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Air, 0x00, 0x00);
                                        else {
                                            int veinDir = world.rand.Next(0, 8);
                                            Block blockToPlace = Block.Stone;
                                            #region Ore
                                            for (int i = 0; i < numOre; i++) {
                                                if (oreType[i] == (byte)Block.DiamondOre || oreType[i] == (byte)Block.GoldOre) {    // Smaller deposits
                                                    if (veinDir <= 2) {         // Cube (2x2)
                                                        if (oreX[i] < bX - 0)
                                                            continue;
                                                        if (oreX[i] > bX + 1)
                                                            continue;
                                                        if (oreY[i] < bY - 0)
                                                            continue;
                                                        if (oreY[i] > bY + 1)
                                                            continue;
                                                        if (oreZ[i] < bZ - 0)
                                                            continue;
                                                        if (oreZ[i] > bZ + 1)
                                                            continue;
                                                    } else if (veinDir <= 4) {  // Long across X
                                                        if (oreX[i] < bX - 0)
                                                            continue;
                                                        if (oreX[i] > bX + 2)
                                                            continue;
                                                        if (oreY[i] < bY - 0)
                                                            continue;
                                                        if (oreY[i] > bY + 1)
                                                            continue;
                                                        if (oreZ[i] < bZ - 0)
                                                            continue;
                                                        if (oreZ[i] > bZ + 1)
                                                            continue;
                                                    } else if (veinDir <= 6) {  // Long across Z
                                                        if (oreX[i] < bX - 0)
                                                            continue;
                                                        if (oreX[i] > bX + 1)
                                                            continue;
                                                        if (oreY[i] < bY - 0)
                                                            continue;
                                                        if (oreY[i] > bY + 1)
                                                            continue;
                                                        if (oreZ[i] < bZ - 0)
                                                            continue;
                                                        if (oreZ[i] > bZ + 2)
                                                            continue;
                                                    } else if (veinDir <= 8) {  // Tall
                                                        if (oreX[i] < bX - 0)
                                                            continue;
                                                        if (oreX[i] > bX + 1)
                                                            continue;
                                                        if (oreY[i] < bY - 0)
                                                            continue;
                                                        if (oreY[i] > bY + 2)
                                                            continue;
                                                        if (oreZ[i] < bZ - 0)
                                                            continue;
                                                        if (oreZ[i] > bZ + 1)
                                                            continue;
                                                    }
                                                } else {                                                                            // Larger deposits
                                                    if (veinDir <= 2) {         // Cube (2x2)
                                                        if (oreX[i] < bX - 0)
                                                            continue;
                                                        if (oreX[i] > bX + 1)
                                                            continue;
                                                        if (oreY[i] < bY - 0)
                                                            continue;
                                                        if (oreY[i] > bY + 1)
                                                            continue;
                                                        if (oreZ[i] < bZ - 0)
                                                            continue;
                                                        if (oreZ[i] > bZ + 1)
                                                            continue;
                                                    } else if (veinDir <= 4) {  // Long across X
                                                        if (oreX[i] < bX - 1)
                                                            continue;
                                                        if (oreX[i] > bX + 2)
                                                            continue;
                                                        if (oreY[i] < bY - 0)
                                                            continue;
                                                        if (oreY[i] > bY + 1)
                                                            continue;
                                                        if (oreZ[i] < bZ - 0)
                                                            continue;
                                                        if (oreZ[i] > bZ + 1)
                                                            continue;
                                                    } else if (veinDir <= 6) {  // Long across Z
                                                        if (oreX[i] < bX - 0)
                                                            continue;
                                                        if (oreX[i] > bX + 1)
                                                            continue;
                                                        if (oreY[i] < bY - 0)
                                                            continue;
                                                        if (oreY[i] > bY + 1)
                                                            continue;
                                                        if (oreZ[i] < bZ - 1)
                                                            continue;
                                                        if (oreZ[i] > bZ + 2)
                                                            continue;
                                                    } else if (veinDir <= 8) {  // Tall
                                                        if (oreX[i] < bX - 0)
                                                            continue;
                                                        if (oreX[i] > bX + 1)
                                                            continue;
                                                        if (oreY[i] < bY - 1)
                                                            continue;
                                                        if (oreY[i] > bY + 2)
                                                            continue;
                                                        if (oreZ[i] < bZ - 0)
                                                            continue;
                                                        if (oreZ[i] > bZ + 1)
                                                            continue;
                                                    }
                                                }
                                                blockToPlace = (Block)oreType[i];
                                                break;
                                            }
                                            #endregion
                                            blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(blockToPlace, 0x00, 0x00);
                                        }
                                    } else
                                        blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Dirt, 0x00, 0x00);
                                } else if (currentHeight == bY) {
                                    if (bY == world.seaLevel || bY == world.seaLevel - 1)
                                        blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Sand, 0x00, 0x00); // FF
                                    else if (bY < world.seaLevel)
                                        blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Gravel, 0x00, 0x00); // FF
                                    else
                                        blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Grass, 0x00, 0x00); // FF
                                } else {
                                    if (bY <= world.seaLevel)
                                        blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Water, 0x00, 0x00); // FF
                                    else
                                        blocks[bY + (bZ * 128 + (bX * 128 * 16))] = new ChunkBlock(Block.Air, 0x00, 0x00); // FF
                                }
                            }
                        }
                    }
                }
                CalculateHeightmap();
            }
            hasChanged = true;
        }

        #endregion

        #region Lighting

        // Loops through all blocks in search of light sources 
        public void CalculateLighting() {
            Logger.Log("Calculating lighting for chunk ({0}, {1})", LogType.Debug, x, z);

            // Loop through all blocks and search for light sources
            for (byte bX = 0; bX < 16; bX++) {
                for (byte bY = 0; bY < 128; bY++) {
                    for (byte bZ = 0; bZ < 16; bZ++) {
                        if(bY >= heightmap[GetHeightmapIndex(bX, bZ)]) {
                            ChunkBlock curBlock = GetBlock(bX, bY, bZ);
                            curBlock.lighting = 0x0F;
                            SetBlock(curBlock, bX, bY, bZ);
                        }
                    }
                }
            }

            terrainPopulated = true;
        }

        #endregion

        #region Heightmap

        internal void CalculateHeightmap() {
            for (byte x = 0; x < 16; x++) {
                for (byte z = 0; z < 16; z++) {
                    for (byte y = 127; y >= 0; y--) {
                        if (Blocks.SkyLightCarryingBlocks.Contains((Block)blocks[GetBlockIndex(x, y, z)].type))
                            continue;
                        heightmap[GetHeightmapIndex(x, z)] = (byte)(y + 1);
                        break;
                    }
                }
            }
        }

        #endregion

        #region Blocks

        public ChunkBlock GetBlock(byte x, byte y, byte z) {
            lock (blockslock) {
                return blocks[GetBlockIndex(x, y, z)];
            }
        }

        public void SetBlock(ChunkBlock block, byte x, byte y, byte z) {
            SetBlock(block, x, y, z, false);
        }

        public void SetBlock(ChunkBlock block, byte x, byte y, byte z, bool calculateLight) {
            lock (blockslock) {
                blocks[GetBlockIndex(x, y, z)] = block;
                hasChanged = true;
                if (calculateLight) {
                    if (y >= heightmap[GetHeightmapIndex(x, z)] && (!Blocks.SkyLightCarryingBlocks.Contains((Block)block.type))) {
                        for (byte hY = (byte)(y - 1); hY >= heightmap[GetHeightmapIndex(x, z)]; hY--) {
                            world.ChangeLight(0x00, GetWorldX(x), hY, GetWorldZ(z));
                        }
                        heightmap[GetHeightmapIndex(x, z)] = (byte)(y + 1);
                    } else if (y == (heightmap[GetHeightmapIndex(x, z)] - 1) && Blocks.SkyLightCarryingBlocks.Contains((Block)block.type)) {
                        for (byte hY = (byte)(heightmap[GetHeightmapIndex(x, z)] - 1); hY >= 0; hY--) {
                            if (!Blocks.SkyLightCarryingBlocks.Contains((Block)blocks[GetBlockIndex(x, hY, z)].type))
                                break;
                            world.ChangeLight(0x0F, GetWorldX(x), hY, GetWorldZ(z));
                            heightmap[GetHeightmapIndex(x, z)] = (byte)(hY);
                        }
                    } else if (y < (heightmap[GetHeightmapIndex(x, z)] - 1))
                        world.ChangeLight(block.lighting, GetWorldX(x), y, GetWorldZ(z));
                }
            }
        }

        #endregion

        #region Block Light

        public byte GetBlockLight(byte x, byte y, byte z) {
            lock (blockslock) {
                return blocks[GetBlockIndex(x, y, z)].lighting;
            }
        }

        public void SetBlockLight(byte value, byte x, byte y, byte z) {
            lock (blockslock) {
                blocks[GetBlockIndex(x, y, z)].lighting = value;
                hasChanged = true;
            }
        }

        #endregion

        #region Players

        public void AddPlayer(Player ply) {
            if (!players.Contains(ply))
                players.Add(ply);
        }

        public void RemovePlayer(Player ply) {
            if (players.Contains(ply))
                players.Remove(ply);
            if (players.Count == 0 && (UseCount <= 0))
                world.UnloadChunk(this);
        }

        #endregion

        #region Utils

        public static string ChunkFilename(int x, int z) {
            string tempX2 = (x < 0 ? "-" : "") + ExpandableHexConverter.ToHex(Math.Abs(x), ExpandableHexConverter.ExpandLevel.Z).ToLower();
            string tempZ2 = (z < 0 ? "-" : "") + ExpandableHexConverter.ToHex(Math.Abs(z), ExpandableHexConverter.ExpandLevel.Z).ToLower();
            return "world/chunks/c." + tempX2 + "." + tempZ2 + ".dat";
        }

        internal static int GetBlockIndex(byte x, byte y, byte z) {
            return y + (z * 128 + (x * 128 * 16));
        }

        internal static int GetHeightmapIndex(byte x, byte z) {
            return z + (x * 16);
        }

        public int GetWorldX(byte bX) {
            int chunkX = x;
            int returnX = bX;
            if (x < 0) returnX -= 16;
            if (returnX < 0) chunkX++;
            returnX = (chunkX * 16) + returnX;
            return returnX;
        }

        public int GetWorldZ(byte bZ) {
            int chunkZ = z;
            int returnZ = bZ;
            if (z < 0) returnZ -= 16;
            if (returnZ < 0) chunkZ++;
            returnZ = (chunkZ * 16) + returnZ;
            return returnZ;
        }

        #endregion
    }
}
